package com.atguigu.tingshu.common.login.aspect;


import com.alibaba.fastjson.JSONObject;
import com.atguigu.tingshu.common.constant.PublicConstant;
import com.atguigu.tingshu.common.constant.RedisConstant;
import com.atguigu.tingshu.common.execption.GuiguException;
import com.atguigu.tingshu.common.login.annotation.TingShuLogin;
import com.atguigu.tingshu.common.util.AuthContextHolder;
import jakarta.servlet.http.HttpServletRequest;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.security.oauth2.resource.OAuth2ResourceServerProperties;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.security.jwt.Jwt;
import org.springframework.security.jwt.JwtHelper;
import org.springframework.security.jwt.crypto.sign.RsaVerifier;
import org.springframework.stereotype.Component;
import org.apache.commons.lang3.StringUtils;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import java.lang.reflect.Method;
import java.util.Map;

/**
 * 切面类:认证中心 --校验权限的逻辑
 */


@Aspect
@Component
public class LoginAspect {


    @Autowired
    private StringRedisTemplate stringRedisTemplate;

/**
 * 切面类:Advisor:
 * 定义切面逻辑(通知)
 * SpringAop的通知类型有5种
 * 通知类型:选择:为了以后业务的扩展一步到位直接使用环绕通知
 * 1)前置通知:切面逻辑方法上添加@Before:在执行目标方法之前 会先进入到前置通知。
 * 2)返回通知:切面逻辑方法上添加@AfterReturinq: 在执行目标方法之后,日执行目标方法不能出现异常，此时才会进入到返回通知
 * 3)异常通知:切面逻辑方法上添加AfterThrowing:在执行目标方法期间 且出现了异常 此时才会进入到异常通知
 * 4)后置通知:切面逻辑方法上添加@After:在执行目标方法之后，且无论执行目标方法期间是否出现了异常 都会进入到后置通知
 * 5)环绕通知:切面逻辑方法上添加@Around: 在执行目标方法之前或者后 且无论执行目标方法期间是否出现了异常 都会进入到环绕通知。
 * 定义切入点表达式:
 * SpringAop的切入点表达式有9(11)种
 * 未来使用注解的切入点表达式模式:@annotatio
 * 在多切面的情况可能出现一些问题。
 * 1)登录切面
 * 2)事务切面(可能会出现事务失效)
 */


    @Around(value = "@annotation(com.atguigu.tingshu.common.login.annotation.TingShuLogin)")
    public Object doBasicProfiling(ProceedingJoinPoint pjp) throws Throwable {

        //前置通知(校验)
        //0.获取前端请求的Token
        String accessToken = getAccessToken();

        //1.判断登录注解的属性(是否需要登录)
         Boolean loginFlag=  isCheckLogin(pjp);
         if (loginFlag==false){
             //不需要登录
             if (StringUtils.isEmpty(accessToken)){ //判断是否携带token ,携带了就放行交给下面的执行
                 return pjp.proceed(); //执行目标方法(放行)
             }
         }

        //2.校验accessToken是否信息完整(accessToken是否被篡改,Redis中的是否过期) &&解析出来载荷的用户id
        Long userId = checkAccessTokenAndGetUserId(accessToken);
        //3.将从jwt中解析的userId,放到ThreadLocal中(因为切面逻辑执行的线程和目标方法(controller)执行的线程是同一个)
        AuthContextHolder.setUserId(userId);
        Object retVal;  //去执行目标方法
        try {
            retVal = pjp.proceed();
        } finally {
            AuthContextHolder.removeUserId(); //防止内存泄露
            //内存泄露不一定会发生
            //ThreadLocal--->Thread线程内部,每一个线程都有一个ThreadLocal对象
            //如果说我们自己创建的线程 new ThreadLocal对象,那么这个线程执行完之后,ThreadLocal对象就会被回收
            //如果是线程池创建的线程,线程池中的线程执行完之后,ThreadLocal对象不会被回收,此时就会导致内存泄露
            //当前是tomcat线程池分配线程执行的//默认tocat线程池分配的线程数是10,最大线程数是200

        }

        return retVal;
    }

    /**
     * 根据登录注解获取登录属性判断是否需要登录
     * 登录注解在目标放方法上,而pjp拥有目标对象的一切信息
     * @param pjp
     * @return
     */
    private Boolean isCheckLogin(ProceedingJoinPoint pjp) {
        //1.得到了一个方法签名 签名接口
        MethodSignature methodSignature = (MethodSignature) pjp.getSignature();
        //2.得到方法对象
        Method method = methodSignature.getMethod();
        //3.得到方法上的注解对象
        TingShuLogin tingShuLoginAnnotation = method.getAnnotation(TingShuLogin.class);
        //4.得到注解中的属性值
        boolean required = tingShuLoginAnnotation.required();
        return required;
    }


    /**
     * 校验accessToken是否信息完整(accessToken是否被篡改,Redis中的是否过期) &&解析出来载荷的用户id
     * @param accessToken
     * @return
     */
    private Long checkAccessTokenAndGetUserId(String accessToken) {
        //校验
        //1.判断前端是否携带了accessToken
        if (StringUtils.isEmpty(accessToken)){
            //为空,code不存在
            throw new GuiguException(201,"token不存在,登录失败");
        }
        //2.判断前端携带的accessToken是否正确(是否被修改,是否过期)
        //2.1 校验jwt的完整性
        Jwt jwt = JwtHelper.decodeAndVerify(accessToken, new RsaVerifier(PublicConstant.PUBLIC_KEY));
        //拿到载荷数据
        String claims = jwt.getClaims();
        //解析
        Map<String, Object> map = JSONObject.parseObject(claims, Map.class);
        Object userId = map.get("userId");
        Object openId = map.get("openId");

        //2.2 是否过期,是否被篡改
        String accessTokenKey= RedisConstant.USER_LOGIN_KEY_PREFIX+openId;
        String fromRedisAccessToken = stringRedisTemplate.opsForValue().get(accessTokenKey);
        if (StringUtils.isEmpty(fromRedisAccessToken)||!fromRedisAccessToken.equals(accessToken)){
            //过期了,
            throw new GuiguException(401,"accessToken过期,登录失败");
        }
        //2.3 没过期,没被篡改
        return Long.parseLong(userId.toString());

    }


    /**
     * 获取请求头中的token
     * @return
     */
    private String getAccessToken() {
        //1.1获取请求属性对象
        ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        //1.2根据请求属性获取请求对象
        HttpServletRequest request = requestAttributes.getRequest();
        //1.3根据请求对象获取请求头
        String accessToken = request.getHeader("token");
        return accessToken;
    }
}
